package zdyu.data_structures.hash_map;

import sun.reflect.generics.reflectiveObjects.NotImplementedException;
import zdyu.data_structures.linked_list.LinkedList;
import zdyu.utils.GenericsArray;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

public class HashMap<K, V> implements java.util.Map<K, V> {

    static final int INITIAL_LENGTH = 8;
    static final float LOAD_FACTOR = 0.75f;

    private int size = 0;

    GenericsArray<LinkedList<Node<K, V>>> bucket = new GenericsArray<>(INITIAL_LENGTH); {
        initializeBucket(this.bucket);
    }

    @Override
    public int size() {
        return this.size;
    }

    @Override
    public boolean isEmpty() {
        return size() == 0;
    }

    @Override
    public boolean containsKey(Object key) {
        for (LinkedList<Node<K, V>> list: bucket) {
            for (Node<K, V> node: list) {
                if (Objects.equals(key, node.key)) {
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    public boolean containsValue(Object value) {
        for (LinkedList<Node<K, V>> list: bucket) {
            for (Node<K, V> node: list) {
                if (Objects.equals(value, node.value)) {
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    public V get(Object key) {
        Node<K, V> node = getNode(key);

        if (node == null) {
            return null;
        } else {
            return node.getValue();
        }
    }

    Node<K, V> getNode(Object key) {
        for (LinkedList<Node<K, V>> list: bucket) {
            for (Node<K, V> node: list) {
                if (Objects.equals(key, node.key)) return node;
            }
        }

        return null;
    }

    @Override
    public V put(K key, V value) {
        if (value == null) return remove(key);

        {
            Node<K, V> node = getNode(key);
            if (node != null) {
                V oldValue = node.value;
                node.value = value;
                return oldValue;
            }
        }

        synchronized (this) {
            putIntoBucket(this.bucket, key, value);
            this.size += 1;

            while (isTooLoaded()) {
                expandToDouble();
            }

            return null;
        }
    }

    private void expandToDouble() {
        GenericsArray<LinkedList<Node<K, V>>> newBucket = new GenericsArray<>((bucket.length() << 1));
        initializeBucket(newBucket);
        for (LinkedList<Node<K, V>> list: this.bucket) {
            for (Node<K, V> node: list) {
                putIntoBucket(newBucket, node);
            }
        }
        this.bucket = newBucket;
    }

    synchronized void putIntoBucket(GenericsArray<LinkedList<Node<K, V>>> bucket, Node<K, V> node) {
        int position = hash(node.key) % bucket.length();
        bucket.get(position).add(node);
    }
    synchronized void putIntoBucket(GenericsArray<LinkedList<Node<K, V>>> bucket, K key, V value) {
        putIntoBucket(bucket, new Node<>(key, value));
    }


    @Override
    public synchronized V remove(Object key) {
        Node<K, V> nodeForKey = getNode(key);
        if (nodeForKey == null) return null;

        int position = hash((K) key) % bucket.length();
        bucket.get(position).remove(nodeForKey);
        this.size -= 1;
        return nodeForKey.getValue();
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        m.forEach(this::put);
    }

    @Override
    public synchronized void clear() {
        bucket.forEach(LinkedList::clear);

        size = 0;
    }

    @Override
    public Set<K> keySet() {
        Set<K> result = new HashSet<>();

        bucket.forEach(list -> list.forEach(node -> result.add(node.key)));

        return result;
    }

    @Override
    public Collection<V> values() {
        List<V> result = new LinkedList<>();

        bucket.forEach(list -> list.forEach(node -> result.add(node.value)));

        return result;
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        Set<Entry<K, V>> result = new HashSet<>();

        bucket.forEach(list -> list.forEach(node -> result.add(new AbstractMap.SimpleEntry<>(node.key, node.value))));

        return result;
    }

    int hash(K key) {
        return (key == null) ? 0 : key.hashCode() & ((1 << 31) - 1);
    }

    int getLoad() {
        AtomicInteger load = new AtomicInteger(0);

        bucket.forEach(list -> {
            if (!list.isEmpty()) load.getAndIncrement();
        });

        return load.get();

    }

    boolean isTooLoaded() {
        return getLoad() >= (int) (LOAD_FACTOR * bucket.length());
    }

    synchronized void initializeBucket(GenericsArray<LinkedList<Node<K, V>>> bucket) {
        IntStream.range(0, bucket.length()).forEach(i -> bucket.put(i, new LinkedList<>()));
    }

    class Node<K, V> {
        private final K key;
        private V value;

        public Node(K key) {
            this.key = key;
        }

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }

        public V getValue() {
            return value;
        }
        public void setValue(V value) {
            this.value = value;
        }

        public K getKey() {
            return key;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Node)) return false;
            Node<?, ?> node = (Node<?, ?>) o;
            return Objects.equals(key, node.key);
        }

        @Override
        public int hashCode() {
            return Objects.hash(key);
        }

        @Override
        public String toString() {
            return new StringJoiner(": ", "(", ")")
                    .add(key.toString())
                    .add(value.toString())
                    .toString();
        }
    }

    @Override
    public String toString() {
        return bucket.toString();
    }
}
